﻿using System;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using System.ComponentModel.Composition;
using FamilyShowSource.Events;
using FamilyShowSource.Views;
using FamilyShowSource.Views.Diagram;

namespace FamilyShowSource.ViewModels
{
    [Export(typeof(DiagramViewModel))]
    public class DiagramViewModel
    {
        #region Fields

        /// <summary>
        /// Defines the constants of this class
        /// </summary>
        private static class Const
        {
            /// <summary>
            /// Amount of the diagram that cannot be scrolled, this 
            /// ensures that some of the diagram is always visible.
            /// </summary>
            public const double PanMargin = 50;

            /// <summary>
            /// Duration to pause before auto centering the diagram.
            /// </summary>
            public const double AutoCenterAnimationPauseDuration = 1000;

            /// <summary>
            /// Duration of the auto center animation.
            /// </summary>
            public const double AutoCenterAnimationDuration = 600;

            /// <summary>
            /// The size of column detail
            /// </summary>
            public const double DetailColSize = 300;
        }

        /// <summary>
        /// Refer to the diagram view
        /// </summary>
        private static DiagramView _diagramView;

        /// <summary>
        /// Diagram Instance in the viewModel
        /// </summary>
        private static Diagram _diagram;

        /// <summary>
        /// Grid Instance in the viewModel
        /// </summary>
        private static Grid _grid;

        /// <summary>
        /// ScrollViewer Instance in the viewModel
        /// </summary>
        private static ScrollViewer _scrollviewer;

        /// <summary>
        /// ScrollViewer Instance in the viewModel
        /// </summary>
        private static Slider _zoomSlider;

        /// <summary>
        /// Stores the top-left offset of the diagram. Used to auto-scroll
        /// the new primary node to the location of the previous selected node.
        /// </summary>
        private static Point _previousTopLeftOffset;

        /// <summary>
        /// Timer that is used when animating a new diagram.
        /// </summary>
        private static DispatcherTimer _autoCenterTimer;

        #endregion

        #region Properties

        /// <summary>
        /// Get or set the zoom level of the diagram.
        /// </summary>
        public static double Zoom
        {
            get { return _zoomSlider.Value; }
            set
            {
                if (value >= _zoomSlider.Minimum && value <= _zoomSlider.Maximum)
                {
                    _diagram.Scale = value;
                    _zoomSlider.Value = value;
                    UpdateScrollSize();
                }
            }
        }
        
        #endregion

        #region Dependency Properties

        /// <summary>
        /// The dependency property is published to retrieve the Digram of the diagram view
        /// </summary>
        public static readonly DependencyProperty DiagramProperty = DependencyProperty.RegisterAttached("Diagram",
                                                                        typeof(Diagram), typeof(DiagramViewModel),
                                                                        new FrameworkPropertyMetadata(OnDiagramChanged));

        /// <summary>
        /// The dependency property is published to retrieve the Grid of the diagram view
        /// </summary>
        public static readonly DependencyProperty GridProperty = DependencyProperty.RegisterAttached("Grid",
                                                                    typeof(Grid), typeof(DiagramViewModel),
                                                                    new FrameworkPropertyMetadata(OnGridChanged));

        /// <summary>
        /// The dependency property is published to retrieve the ScrollViewer of the diagram view
        /// </summary>
        public static readonly DependencyProperty ScrollViewerProperty = DependencyProperty.RegisterAttached("ScrollViewer",
                                                                            typeof(ScrollViewer), typeof(DiagramViewModel),
                                                                            new FrameworkPropertyMetadata(OnScrollViewerChanged));

        /// <summary>
        /// The dependency property is published to retrieve the ScrollViewer of the diagram view
        /// </summary>
        public static readonly DependencyProperty ZoomSliderProperty = DependencyProperty.RegisterAttached("ZoomSlider",
                                                                            typeof(Slider), typeof(DiagramViewModel),
                                                                            new FrameworkPropertyMetadata(OnZoomSliderChanged));

        /// <summary>
        /// The dependency property is published to retrieve the ScrollViewer of the diagram view
        /// </summary>
        public static readonly DependencyProperty DiagramViewProperty = DependencyProperty.RegisterAttached("DiagramView",
                                                                            typeof(DiagramView), typeof(DiagramViewModel),
                                                                            new FrameworkPropertyMetadata(OnDiagramViewChanged));

        /// <summary>
        /// Method to get the Diagram of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        public static Diagram GetDiagram(DependencyObject element)
        {
            return (Diagram)element.GetValue(DiagramProperty);
        }

        /// <summary>
        /// Method to set the Diagram of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        public static void SetDiagram(DependencyObject element, Diagram value)
        {
            element.SetValue(DiagramProperty, value);
        }

        /// <summary>
        /// Method to get the Grid of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        public static Grid GetGrid(DependencyObject element)
        {
            return (Grid)element.GetValue(GridProperty);
        }

        /// <summary>
        /// Method to set the Grid of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        public static void SetGrid(DependencyObject element, Grid value)
        {
            element.SetValue(GridProperty, value);
        }

        /// <summary>
        /// Method to get the ScrollViewer of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        public static ScrollViewer GetScrollViewer(DependencyObject element)
        {
            return (ScrollViewer)element.GetValue(ScrollViewerProperty);
        }

        /// <summary>
        /// Method to set the ScrollViewer of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        public static void SetScrollViewer(DependencyObject element, ScrollViewer value)
        {
            element.SetValue(ScrollViewerProperty, value);
        }

        /// <summary>
        /// Method to get the ZoomSlider of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        public static Slider GetZoomSlider(DependencyObject element)
        {
            return (Slider)element.GetValue(ZoomSliderProperty);
        }

        /// <summary>
        /// Method to set the ZoomSlider of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        public static void SetZoomSlider(DependencyObject element, Slider value)
        {
            element.SetValue(ZoomSliderProperty, value);
        }

        /// <summary>
        /// Method to get the DiagramView of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <returns></returns>
        public static DiagramView GetDiagramView(DependencyObject element)
        {
            return (DiagramView)element.GetValue(DiagramViewProperty);
        }

        /// <summary>
        /// Method to set the DiagramView of the diagram view
        /// </summary>
        /// <param name="element"></param>
        /// <param name="value"></param>
        public static void SetDiagramView(DependencyObject element, DiagramView value)
        {
            element.SetValue(DiagramViewProperty, value);
        }

        #endregion

        #region Constructors
        
        /// <summary>
        /// The default constructor
        /// </summary>
        [ImportingConstructor]
        public DiagramViewModel()
        {
            _autoCenterTimer = new DispatcherTimer();
            SubscribeNotifyDiagramPopulatedEvent();
        }
        
        #endregion

        #region Methods

        #region Dependency Methods

        /// <summary>
        /// A reference to a handler implementation that the property system will call 
        /// whenever the effective value of the property changes.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        public static void OnDiagramChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            _diagram = obj as Diagram;
            if(_diagram != null)
                _diagram.SizeChanged += DiagramSizeChanged;
        }

        /// <summary>
        /// A reference to a handler implementation that the property system will call 
        /// whenever the effective value of the property changes.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        public static void OnGridChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            _grid = obj as Grid;
        }

        /// <summary>
        /// A reference to a handler implementation that the property system will call 
        /// whenever the effective value of the property changes.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        public static void OnScrollViewerChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            _scrollviewer = obj as ScrollViewer;
        }

        /// <summary>
        /// A reference to a handler implementation that the property system will call 
        /// whenever the effective value of the property changes.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        public static void OnZoomSliderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            _zoomSlider = obj as Slider;
        }

        /// <summary>
        /// A reference to a handler implementation that the property system will call 
        /// whenever the effective value of the property changes.
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="args"></param>
        public static void OnDiagramViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            _diagramView = obj as DiagramView;
            if (_diagramView != null)
                _diagramView.SizeChanged += DiagramSizeChanged;
        }

        /// <summary>
        /// This method is invoked when the size of diagram view has been changed
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void DiagramSizeChanged(object sender, SizeChangedEventArgs e)
        {
            UpdateScrollSize();
        }

        #endregion

        #region Publishes & Subscribes

        /// <summary>
        /// Subscribe the event to handle diagram populated
        /// </summary>
        private static void SubscribeNotifyDiagramPopulatedEvent()
        {
            EventAggregatorFactory.Instance.GetEvent<NotifyDiagramPopulatedEvent>().Subscribe(eventData =>
            {
                // Save the current top-left offset before force the layout,
                // this is used later when animating the diagram.
                var offset = GetTopLeftScrollOffset();

                _previousTopLeftOffset = new Point(
                    _grid.ActualWidth - _scrollviewer.HorizontalOffset - offset.X,
                    _grid.ActualHeight - _scrollviewer.VerticalOffset - offset.Y);

                // The diagram was populated. Need to force the diagram to layout 
                // since the diagram values are required to perform animations (need 
                // to know exactly where the primary node is located).
                _diagramView.UpdateLayout();

                // Now auto-scroll so the primary node appears at the previous selected node location.
                AutoScrollToSelected();
                // Reset the time slider.
                //TimeSlider.Value = TimeSlider.Maximum;
            }, false);
        }

        #endregion

        #region Diagram Updated

        /// <summary>
        /// Move the viewport so the primary node appears at the location of the selected node.
        /// </summary>
        private static void AutoScrollToSelected()
        {
            // Don't scroll if the diagram is empty.
            if (_diagram.ActualWidth == 0 || _diagram.ActualHeight == 0)
                return;

            // This is the offset that will be scrolled. First get the offset that positions the diagram in the top-left corner.
            var offset = GetTopLeftScrollOffset();

            // Get the location of the node that was selected.
            var selectedBounds = _diagram.SelectedNodeBounds;

            // See if this is the first time the diagram is being displayed.            
            if (selectedBounds.IsEmpty)
            {
                // First time, center the diagram in the display area.
                offset.X += (_diagramView.ActualWidth - Const.DetailColSize - (_diagram.ActualWidth * Zoom)) / 2;
            }
            else
            {
                // Offset the distance the diagram is scrolled from the previous top-left position.
                offset.X += _previousTopLeftOffset.X;
                offset.Y += _previousTopLeftOffset.Y;

                // Determine the distance between the two nodes.
                var primaryBounds = _diagram.PrimaryNodeBounds;
                var nodeDelta = new Point
                {
                    X = (primaryBounds.Left + (primaryBounds.Width/2)) - (selectedBounds.Left + (selectedBounds.Width/2)),
                    Y = (primaryBounds.Top + (primaryBounds.Height/2)) - (selectedBounds.Top + (selectedBounds.Height/2))
                };

                // Offset the distance between the two nodes.
                offset.X -= (nodeDelta.X * Zoom);
                offset.Y -= (nodeDelta.Y * Zoom);
            }

            // Scroll the viewport to the selected node.
            _scrollviewer.ScrollToHorizontalOffset(_grid.Width - offset.X);

            // Set a timer so there is a pause before centering the diagram.
            _autoCenterTimer.Interval = App.GetAnimationDuration(Const.AutoCenterAnimationPauseDuration);
            _autoCenterTimer.Tick += OnAutoCenterPauseTimer;
            _autoCenterTimer.IsEnabled = true;
        }

        /// <summary>
        ///Pause animation before centering the diagram.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnAutoCenterPauseTimer(object sender, EventArgs e)
        {
            // Timer only fires once.
            _autoCenterTimer.IsEnabled = false;
            // Scroll the diagram so it's centered in the display area.
            AutoScrollToCenter();
        }

        /// <summary>
        /// Center the diagram in the display area.
        /// </summary>
        private static void AutoScrollToCenter()
        {
            // Adjust the offset so the diagram appears in the center of the display area. First get the top-left offset.
            var offset = GetTopLeftScrollOffset();

            // Now adjust the offset so the diagram is centered.
            offset.Y += (_diagramView.ActualHeight - (_diagram.ActualHeight * Zoom)) / 2;

            // Auto scroll the diagram.
            _scrollviewer.ScrollToVerticalOffset(offset.Y - _grid.Height);

            // Before auto scroll, determine the start and end points so the scrolling can be animated.
            var endLocation = new Point(0, offset.Y);
            // Animate the scrollings.
            AnimateDiagram(endLocation);
        }

        /// <summary>
        /// Return the offset that positions the diagram in the top-left 
        /// corner, takes into account the zoom level.
        /// </summary>
        private static Point GetTopLeftScrollOffset()
        {
            // Offset that is returned.
            var offset = new Point();

            // Empty offset if the diagram is empty.
            if (_diagram.ActualWidth == 0 || _diagram.ActualHeight == 0)
                return offset;

            // Get the size of the diagram.
            var diagramSize = new Size(_diagram.ActualWidth * Zoom, _diagram.ActualHeight * Zoom);

            // Calcualte the offset that positions the diagram in the top-left corner.
            offset.X = _diagramView.ActualWidth - Const.DetailColSize + diagramSize.Width;
            offset.Y = diagramSize.Height;

            return offset;
        }

        /// <summary>
        /// Animate the diagram by moving from the startLocation to the endLocation.
        /// </summary>
        private static void AnimateDiagram(Point endLocation)
        {
            // Create the animations, nonlinear by using accelration and deceleration.
            var horzAnim = new DoubleAnimation(0, endLocation.X, App.GetAnimationDuration(Const.AutoCenterAnimationDuration))
            {
                AccelerationRatio = .5,
                DecelerationRatio = .5
            };

            var vertAnim = new DoubleAnimation(0, endLocation.Y, App.GetAnimationDuration(Const.AutoCenterAnimationDuration))
            {
                AccelerationRatio = .5,
                DecelerationRatio = .5
            };

            // Animate the transform to make it appear like the diagram is moving.
            var transform = new TranslateTransform();
            //transform.BeginAnimation(TranslateTransform.XProperty, horzAnim);
            transform.BeginAnimation(TranslateTransform.YProperty, vertAnim);

            // Animate the grid (that contains the diagram) instead of the 
            // diagram, otherwise nodes are clipped in the diagram.
            _grid.RenderTransform = transform;
        }

        /// <summary>
        /// Update the scroll area so the diagram can be scrolled from edge to edge.
        /// </summary>
        private static void UpdateScrollSize()
        {
            // Nothing to do if the diagram is empty.
            if (_diagramView.ActualWidth == 0 || _diagramView.ActualHeight == 0)
                return;
            // The grid contains the diagram, set the size of the grid so it's
            // large enough to allow the diagram to scroll from edge to edge.
            _grid.Width = Math.Max(0, (_diagramView.ActualWidth - Const.DetailColSize) * 2);
            _grid.Height = Math.Max(0, _diagramView.ActualHeight * 2);
        }

        #endregion

        #endregion
    }
}
